package ohwmeter;

import gui.KeyUserFetchWindow;
import gui.ResultsWindow;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JButton;

import utils.Statistics;

import de.umass.lastfm.Artist;
import de.umass.lastfm.CallException;
import de.umass.lastfm.Caller;
import de.umass.lastfm.Chart;
import de.umass.lastfm.ResponseBuilder;
import de.umass.lastfm.Track;
import de.umass.lastfm.User;
import de.umass.lastfm.cache.Cache;
import de.umass.lastfm.cache.FileSystemCache;

import static de.umass.util.StringUtilities.map;

public class OneHitWonderMeterAL implements ActionListener, Runnable {

	private boolean usingOutlier;
	
	private KeyUserFetchWindow kufw;
	private String key;
	private String userName;
	
	private final int TOP_TRACKS_NUMBER = 10;
	
	public OneHitWonderMeterAL(String key) {
		this.key = key;
	}
	
	@Override
	public void actionPerformed(ActionEvent e) {
		if(((JButton)e.getSource()) == kufw.getOutlierButton())
			usingOutlier = true;
		else
			usingOutlier = false;
		new Thread(this).start();
	}

	public void registerWindow(KeyUserFetchWindow kufw) {
		this.kufw = kufw;
	}
	
	private void metricStdDev(Collection<Artist> topArtists, Map<Artist, Collection<Track>> topTracks) {
		ResultsWindow rw = new ResultsWindow("One-hit wonderness of " + userName);
	    
	    //Used to calculate the std. dev.
	    Statistics statistics;
	    //Stores the play count for every track. 
	    List<Double> data = new ArrayList<Double>();
	    //Stores the total play count for the top tracks being used.
	    double sum;
	    //Used for printing.
	    int artistCount = 0;
	    
	    //These are used for the average ohw ratio.
	    double valuesSum = 0;
	    int validArtists = 0;
	    
	    for(Artist artist : topArtists) {
	    	sum = 0;
	    	data.clear();
	    	if(topTracks.containsKey(artist)) {
	    		validArtists++;
		    	
	    		//Too few tracks.
	    		if(topTracks.get(artist).size() < TOP_TRACKS_NUMBER)
	    			continue;
	    		
		    	for(Track track : topTracks.get(artist)) {
		    		data.add((double)track.getPlaycount());
		    		sum+=track.getPlaycount();
		    	}
		    	
		    	//Some kind of fetch problem, ignoring...
		    	if(data.isEmpty())
		    		continue;
		    	
		    	rw.println(artistCount+1 + ". " + artist.getName());
		    	artistCount++;
		    	
		    	statistics = new Statistics(data.toArray(new Double[data.size()]));
		    	double value = statistics.getStdDev()/sum; //lim sqrt((0+n^2)/2 - ((0+n)/2)^2)/n, worst case == 1/2
		    	valuesSum+= value;
		    	
		    	//WEKA cluster centroids: 0.0285; 0.0453; 0.0752; 0.1179.
		    	
		    	rw.println("One-hit wonderness: " + value);
		    	rw.print("Rating: ");
		    	if(value >= 0.09655)
		    		rw.println("ONE-HIT-WONDER");
		    	else if(value >= 0.06025)
		    		rw.println("MAJOR HITS ARTIST");
		    	else if(value >= 0.0369)
		    		rw.println("WELL DISTRIBUTED");
		    	else
		    		rw.println("VERY WELL DISTRIBUTED");
		    	rw.println("_____________________________");
	    	}
	    }
	    
	    double value = valuesSum/validArtists;
	    
	    //100/0.09655 == 1035
    	int ohwness = (int)((valuesSum*1035)/validArtists);
    	if(ohwness > 100)
    		ohwness = 100;
    	
	    rw.println("Overall one-hit wonderness: " + ohwness + "/100");
    	rw.print("Overall rating: ");
    	if(value >= 0.09655)
    		rw.println("ONE-HIT-WONDERS");
    	else if(value >= 0.06025)
    		rw.println("MAJOR HITS ARTISTS");
    	else if(value >= 0.0369)
    		rw.println("WELL DISTRIBUTED");
    	else
    		rw.println("VERY WELL DISTRIBUTED");
    	
	    rw.adjustSize();
	}
	
	private void metricOutliers(Collection<Artist> topArtists, Map<Artist, Collection<Track>> topTracks) {
		ResultsWindow rw = new ResultsWindow("One-hit wonderness of " + userName);
	    
	    //Used to calculate the mean and std. dev. values.
	    Statistics statistics;
	    //Stores the play count for every track. 
	    List<Double> data = new ArrayList<Double>();
	    
	    //Used for printing.
	    int artistCount = 0;
	    
	    //These are used for the average ohw ratio.
	    int valuesSum = 0;
	    int validArtists = 0;
	    
	    for(Artist artist : topArtists) {
	    	data.clear();
	    	if(topTracks.containsKey(artist)) {
	    		validArtists++;
		    	
	    		//Too few tracks.
	    		if(topTracks.get(artist).size() < TOP_TRACKS_NUMBER)
	    			continue;
	    		
		    	for(Track track : topTracks.get(artist)) {
		    		data.add((double)track.getPlaycount());
		    	}
		    	
		    	//Some kind of fetch problem, ignoring...
		    	if(data.isEmpty())
		    		continue;
		    	
		    	rw.println(artistCount+1 + ". " + artist.getName());
		    	artistCount++;
		    	
		    	statistics = new Statistics(data.toArray(new Double[data.size()]));
		    	double mean = statistics.getMean();
		    	
		    	double highOutlierLimit = mean + 2.5*mean;
		    	double midOutlierLimit = mean + 1.25*mean;
		    	double lowOutlierLimit = mean + 0.5*mean;
		    	double aboveAvg = mean;
		    	
		    	int highOutlierCount = 0;
		    	int midOutlierCount = 0;
		    	int lowOutlierCount = 0;
		    	int aboveAvgCount = 0;
		    	
		    	for(Track track : topTracks.get(artist)) {
		    		if(track.getPlaycount() > highOutlierLimit) {
		    			highOutlierCount++;
		    			midOutlierCount++;
		    			lowOutlierCount++;
		    			aboveAvgCount++;
		    		}
		    		else if (track.getPlaycount() > midOutlierLimit) {
		    			midOutlierCount++;
		    			lowOutlierCount++;
		    			aboveAvgCount++;
		    		}
		    		else if(track.getPlaycount() > lowOutlierLimit) {
		    			lowOutlierCount++;
		    			aboveAvgCount++;
		    		}
		    		else if(track.getPlaycount() > aboveAvg)
		    			aboveAvgCount++;
		    	}
		    	
		    	//System.out.println(artist.getName() + " --- HO: " + highOutlierCount + ", MO: " + midOutlierCount + ", LO: " + lowOutlierCount + ", AA: " + aboveAvgCount + ";");
		    	
		    	/*
		    	 * Classifications are:
		    	 * 3: ONE-HIT-WONDER
		    	 * 2: MAJOR HITS ARTIST
		    	 * 1: WELL DISTRIBUTED
		    	 * 0: VERY WELL DISTRIBUTED
		    	 */
		    	
		    	//Previous manual algorithm
		    	/*
			    	if(highOutlierCount > 0) {
			    		oneHitWonderRank = 3;
			    		valuesSum += 3;
			    		}
			    	else if(midOutlierCount > 0 || lowOutlierCount > 2) {
			    		oneHitWonderRank = 2;
			    		valuesSum += 2;
			    	}
			    	else if (lowOutlierCount > 1 || aboveAvgCount < 4) {
			    		oneHitWonderRank = 1;
			    		valuesSum += 1;
			    	}
			    	else {
			    		oneHitWonderRank = 0;
			    	}
		    	 */
		    	
		    	/*Algorithm based on the results of the WEKA BFTree classifier.
		    	 * Database with 50 global artists and 50 hyped artists.
		    	 * Manual pre-classification. VW merged with WD because analysis has shown the difference is irrelevant.
		    	 * Correctly Classified Instances 84.8485% (tested with training set)
		    	 * Data retrieval date: June 22, 2013
		    	 */
		    	
		    	short oneHitWonderRank = -1;
		    	
		    	if(midOutlierCount < 0.5) {
		    		if(lowOutlierCount < 1.5) {
		    			oneHitWonderRank = 1;
		    			valuesSum += 1;
		    		}
		    		else if(lowOutlierCount >= 1.5) {
		    			oneHitWonderRank = 2;
		    			valuesSum += 2;
		    		}
		    	}
		    	else if(midOutlierCount >= 0.5) {
		    		if(highOutlierCount < 0.5) {
		    			oneHitWonderRank = 2;
		    			valuesSum += 2;
		    		}
		    		else if(highOutlierCount >= 0.5) {
		    			if(midOutlierCount < 1.5) {
		    				if(aboveAvgCount < 2.5) {
		    					if(lowOutlierCount < 1.5) {
		    						oneHitWonderRank = 3;
					    			valuesSum += 3;
		    					}
		    					else if(lowOutlierCount >= 1.5) {
		    						oneHitWonderRank = 2;
					    			valuesSum += 2;
		    					}
		    				}
		    				else if(aboveAvgCount >= 2.5) {
		    					oneHitWonderRank = 3;
				    			valuesSum += 3;
		    				}
		    			}
		    			else if(midOutlierCount >= 1.5) {
		    				oneHitWonderRank = 2;
			    			valuesSum += 2;
		    			}
		    		}
		    	}
		    	
		    	rw.print("Rating: ");
		    	switch(oneHitWonderRank) {
		    		case 3:
		    			rw.println("ONE-HIT-WONDER");
		    			break;
		    		case 2:
		    			rw.println("MAJOR HITS ARTIST");
		    			break;
		    		case 1:
		    			rw.println("WELL DISTRIBUTED");
		    			break;
		    		case 0:
		    			rw.println("VERY WELL DISTRIBUTED");
		    			break;
		    		default:
		    			rw.println("INVALID CLASSIFICATION");
		    	}
		    	rw.println("_____________________________");
	    	}
	    }
	    
	    double value = (double)valuesSum/(double)validArtists;
    	rw.println("Overall rating: ");
    	if(value > 2.5)
    		rw.println("ONE-HIT-WONDERS");
    	else if(value > 1.5)
    		rw.println("MAJOR HITS ARTISTS");
    	else if(value > 0.5)
    		rw.println("WELL DISTRIBUTED");
    	else
    		rw.println("VERY WELL DISTRIBUTED");
    	rw.println("One-hit wonderness: " + (int)(value*40) + "/100");
    	
	    rw.adjustSize();
	}

	@Override
	public void run() {
		if(kufw.getUser().isEmpty()) return;
		kufw.isFetchingVisibleWithText(true, "Fetching data from last.fm...");
		kufw.areButtonsVisible(false);
		kufw.isUserInputEditable(false);
		
		//Setup the cache directory.
	    File dataFolder = new File("./cache");
	    if (!dataFolder.isDirectory()) {
	        dataFolder.mkdir();
	    }
	    Caller.getInstance().setCache(new FileSystemCache(dataFolder));
	    
	    //Fetches the user name.
	    userName = kufw.getUser();
	    
	    //Get the top artists of the user.
	    Collection<Artist> topArtists;
	    //Calls last.fm.
	    if(userName.toLowerCase().equals("global stats")) {
	    	//200 top artists.
	    	topArtists = Chart.getTopArtists(key).getPageResults();
	    	topArtists.addAll(Chart.getTopArtists(2, key).getPageResults());
	    	topArtists.addAll(Chart.getTopArtists(3, key).getPageResults());
	    	topArtists.addAll(Chart.getTopArtists(4, key).getPageResults());
	    }
	    else if(userName.toLowerCase().equals("hyped artists")) {
	    	//100 hyped artists.
	    	topArtists = Chart.getHypedArtists(key).getPageResults();
	    	topArtists.addAll(Chart.getHypedArtists(2, key).getPageResults());
	    }
	    else {
	    	topArtists = User.getTopArtists(userName, key);
	    	if(topArtists.isEmpty()) {
	    		kufw.isFetchingVisibleWithText(true, "Invalid user name.");
	    		kufw.areButtonsVisible(true);
	    		kufw.isUserInputEditable(true);
	    		return;
	    	}
	    }
	    
	    Map<Artist, Collection<Track>> topTracks = new HashMap<Artist, Collection<Track>>();
	    
	    //Get the top tracks for every top artist of the user.
	    for (Artist artist : topArtists) {
	    	try {
	    		//This is a last.fm requirement. Only 5 calls per second to the API.
                String cacheEntryName = Cache.createCacheEntryName("artist.getTopTracks", map("artist", artist.getName(), "limit", Integer.toString(TOP_TRACKS_NUMBER)));
				if(!Caller.getInstance().getCache().contains(cacheEntryName))
						Thread.sleep(200);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
	    	
	    	Collection<Track> topTracksTemp = null;
	    	try {
	    		//Calls last.fm. Building the call manually so I can specify a limit.
	    		topTracksTemp = ResponseBuilder.buildCollection(Caller.getInstance().call("artist.getTopTracks", key, "artist", artist.getName(), "limit", Integer.toString(TOP_TRACKS_NUMBER)), Track.class);
	    	}
	    	catch(CallException ce) {
	    		System.out.println("Artist \"" + artist.getName() + "\" could not have it's top tracks fetched (XML parse error)");
	    	}
	    	if(topTracksTemp != null)
	    		topTracks.put(artist, topTracksTemp);
	    }
	    
	    kufw.dispose();
	    //Prints the results of the chosen metric on a ResultWindow.
	    if(usingOutlier)
	    	metricOutliers(topArtists, topTracks);
	    else
	    	metricStdDev(topArtists, topTracks);
	}

}
